﻿using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.MSAS.ManagedAPI;
using System.Data.Linq;
using MRS.FrontEnd.BO;

//
// Details about the interaction between the Delivery Service 
// and the MSASAPI as it is done below can be found in the following
// documents:
// http://sharepoint/sites/reputation/Shared%20Documents/Projects/Dev/MSAS%20-%20Frontend%20-%20Design.docx 
// http://sharepoint/sites/reputation/Shared%20Documents/Projects/Dev/MSAS%20API%20Bloom%20Callback%20Sequences.vsd 
namespace Microsoft.Reputation.Services
{
    public class CacheRequestProcessorContext
    {
        public bool SearchDB { get; set; }
        public ICacheProvider Cache { get; private set; }
        public ObjectType ObjectType { get; private set; }
        public List<UnpackedObject> FoundObjects { get; private set; }

        public CacheRequestProcessorContext(ICacheProvider cache, ObjectType objectType)
        {
            this.Cache = cache;
            this.ObjectType = objectType;
            this.FoundObjects = new List<UnpackedObject>();
        }
    }

    public class DatabaseRequestProcessorContext
    {
        public List<ObjectIdentifier> ObjectIdentifiers { get; set; }

        public DatabaseRequestProcessorContext()
        {
            this.ObjectIdentifiers = new List<ObjectIdentifier>();
        }
    }

    public class ResponseProcessorContext
    {
        public ObjectType ObjectType { get; private set; }
        public ICollection<UnpackedObject> MatchesIn { get; private set; }
        public List<UnpackedObject> MatchesOut { get; internal set; }

        public ResponseProcessorContext(ObjectType objectType, ICollection<UnpackedObject> matches)
        {
            this.ObjectType = objectType;
            this.MatchesIn = matches;
            this.MatchesOut = new List<UnpackedObject>();
        }
    }

    public class MsasDataBlockProcessor
    {
        private static int s_noMatchCacheTime = DeliveryServiceConfig.NoMatchExpirationMinutes;
        private IMsasApiRequestProcessor m_requestProcessor;
        private byte[] m_requestData;
        private ICacheProvider m_cache;

        public MsasDataBlockProcessor(IMsasApiRequestProcessor processor, byte[] requestData)
        {
            this.m_requestProcessor = processor;
            this.m_requestData = requestData;
        }

        public MsasDataBlockProcessor(IMsasApiRequestProcessor processor, byte[] requestData, ICacheProvider cache)
            : this(processor, requestData)
        {
            this.m_cache = cache;
        }

        public CacheRequestProcessorContext ProcessRequestForCache(ObjectType objectType)
        {
            CacheRequestProcessorContext context = null;
            try
            {
                context = new CacheRequestProcessorContext(m_cache, objectType);

                MsasProcessRequestProc proc = new MsasProcessRequestProc(MsasDataBlockProcessor.HandleRequestChunkForCache);

                uint hresult = m_requestProcessor.ProcessRequest(m_requestData, context, proc, MRS_ProcessRequest_Flags.MSAS_ProcessRequestFlag_None);

                LogEventToNTLog(hresult, "ProcessRequestForCache");
            }
            catch (Exception ex)
            {
                EventLogger.LogMrsProcessCacheApiFailure(ex.Message);
                TraceUtility.TraceCatchException(Tracing.Source, ex);
            }
            return context;
        }

        public DatabaseRequestProcessorContext ProcessRequestForDatabase()
        {
            DatabaseRequestProcessorContext context = null;
            try
            {
                context = new DatabaseRequestProcessorContext();

                MsasProcessRequestProc proc = new MsasProcessRequestProc(MsasDataBlockProcessor.HandleRequestChunkForDatabase);

                uint hresult = m_requestProcessor.ProcessRequest(m_requestData, context, proc, MRS_ProcessRequest_Flags.MSAS_ProcessRequestFlag_EnumOnly);

                LogEventToNTLog(hresult, "ProcessRequestForDatabase");
            }
            catch (Exception ex)
            {
                EventLogger.LogMrsProcessDatabaseApiFailure(ex.Message);
                TraceUtility.TraceCatchException(Tracing.Source, ex);
            }
            return context;
        }


        public ResponseProcessorContext ProcessRequestForResponse(ObjectType objectType, ICollection<UnpackedObject> matches)
        {
            ResponseProcessorContext context = null;
            try
            {
                context = new ResponseProcessorContext(objectType, matches);
                
                MsasProcessRequestProc proc = new MsasProcessRequestProc(MsasDataBlockProcessor.HandleRequestChunkForResponse);

                uint hresult = m_requestProcessor.ProcessRequest(m_requestData, context, proc, MRS_ProcessRequest_Flags.MSAS_ProcessRequestFlag_None);

                LogEventToNTLog(hresult, "ProcessRequestForResponse");
            }
            catch (Exception ex)
            {
                EventLogger.LogMrsProcessResponseApiFailure(ex.Message);
                TraceUtility.TraceCatchException(Tracing.Source, ex);
            }
            return context;
        }

        internal static uint HandleRequestChunkForCache(
            IntPtr bufferPtr, UInt32 bufferLength, IntPtr noMatchPtr, IntPtr childFilterPtr, UInt32 childFilterLength, IntPtr pvContext)
        {
            if (CheckInputs(bufferPtr, childFilterPtr, true, noMatchPtr, true, pvContext) != MRS_HRESULTS.S_OK)
            {
                // TODO Log an error and return a proper HRESULT
                return MRS_HRESULTS.S_FALSE;
            }
            uint hResult = MRS_HRESULTS.S_OK;

            try
            {
                CacheRequestProcessorContext context = (CacheRequestProcessorContext)GCHandle.FromIntPtr(pvContext).Target;

                byte[] hash = new byte[bufferLength];
                Marshal.Copy(bufferPtr, hash, 0, (int)bufferLength);

                UnpackedObject matchEntry;
                context.Cache.GetMatchedEntry(new ObjectIdentifier { Type = context.ObjectType, Sha256 = hash }, out matchEntry);

                if (matchEntry != null)
                {
                    // We must add the matches to the begining of the list because the Client API
                    // Creates the datablock with the most specific to most generic.  Then it calls 
                    // the callback in the order of most generic to most specific.  This means that 
                    // we will walk through the most specific variant last so it should be at the 
                    // begining of the list.
                    context.FoundObjects.Insert(0, matchEntry);

                    // If there is no bloom it is a No-Match that we have cached 
                    // because it appeared in the bloom
                    if ((!matchEntry.Metadatas.ContainsKey(UrlBinaryCodec.MetadataNameBloom)
                         || matchEntry.Metadatas[UrlBinaryCodec.MetadataNameBloom] == null)
                        && matchEntry.Reputations.Count == 0)
                    {
                        // Set noMatch to true
                        Marshal.WriteInt32(noMatchPtr, 1);
                    }
                    else
                    {
                        byte[] bloom = matchEntry.Metadatas[UrlBinaryCodec.MetadataNameBloom] as byte[];

                        // Set noMatch to false
                        Marshal.WriteInt32(noMatchPtr, 0);
                        Marshal.Copy(bloom, 0, childFilterPtr, (int)childFilterLength);
                    }
                    hResult = MRS_HRESULTS.S_OK;
                }
                else
                {
                    context.SearchDB = true;

                    // Set noMatch to false
                    Marshal.WriteInt32(noMatchPtr, 0);
                    hResult = MRS_HRESULTS.S_FALSE;
                }
                LogEventToNTLog(hResult, "HandleRequestChunkForCache");
            }
            catch (Exception ex)
            {
                EventLogger.LogMrsCallbackCacheApiFailure(ex.Message);
                TraceUtility.TraceCatchException(Tracing.Source, ex);

                // TODO Set some form of hResult
                hResult = MRS_HRESULTS.S_FALSE;
            }
            return hResult;
        }

        internal static uint HandleRequestChunkForDatabase(
            IntPtr bufferPtr, UInt32 bufferLength, IntPtr noMatchPtr, IntPtr childFilterPtr, UInt32 filterLength, IntPtr pvContext)
        {
            if (CheckInputs(bufferPtr, childFilterPtr, false, noMatchPtr, false, pvContext) != MRS_HRESULTS.S_OK)
            {
                LogEventToNTLog(MRS_HRESULTS.S_FALSE, "HandleRequestChunkForDatabase");

                // TODO return a proper HRESULT
                return MRS_HRESULTS.S_FALSE;
            }

            uint hResult = MRS_HRESULTS.S_OK;

            try
            {
                DatabaseRequestProcessorContext context = (DatabaseRequestProcessorContext)GCHandle.FromIntPtr(pvContext).Target;
                byte[] hash = new byte[bufferLength];
                Marshal.Copy(bufferPtr, hash, 0, (int)bufferLength);

                context.ObjectIdentifiers.Add(new ObjectIdentifier { Type = ObjectType.URL, Sha256 = hash });

                LogEventToNTLog(hResult, "HandleRequestChunkForDatabase");
            }
            catch (Exception ex)
            {
                EventLogger.LogMrsCallbackDatabaseApiFailure(ex.Message);
                TraceUtility.TraceCatchException(Tracing.Source, ex);

                // TODO Set some form of hResult
                hResult = MRS_HRESULTS.S_FALSE;
            }
            return hResult;
        }

        internal static uint HandleRequestChunkForResponse(
            IntPtr bufferPtr, UInt32 bufferLength, IntPtr noMatchPtr, IntPtr childFilterPtr, UInt32 childFilterLength, IntPtr pvContext)
        {
            if (CheckInputs(bufferPtr, childFilterPtr, true, noMatchPtr, true, pvContext) != MRS_HRESULTS.S_OK)
            {
                LogEventToNTLog(MRS_HRESULTS.S_FALSE, "HandleRequestChunkForResponse");

                // TODO return a proper HRESULT
                return MRS_HRESULTS.S_FALSE;
            }

            uint hResult = MRS_HRESULTS.S_OK;

            try
            {
                ResponseProcessorContext context = (ResponseProcessorContext)GCHandle.FromIntPtr(pvContext).Target;

                byte[] hash = new byte[bufferLength];
                Marshal.Copy(bufferPtr, hash, 0, (int)bufferLength);

                // Look for the SHA256 in the objects returned from the database
                UnpackedObject matchEntry = null;

                if (context.MatchesIn != null)
                {
                    foreach (UnpackedObject unpackedObj in context.MatchesIn)
                    {
                        if (unpackedObj.Type == ObjectType.URL && unpackedObj.Sha256.SequenceEqual(hash))
                        {
                            matchEntry = unpackedObj;
                            break;
                        }
                    }
                }
                // There are two cases here
                // If we found a matchEntry it means that we got some sort of data back
                // from the database.  This should always have a bloom because it means
                // we knew about the variant.  It may not have category data.
                if (matchEntry != null)
                {

                    if (matchEntry.Metadatas.ContainsKey(UrlBinaryCodec.MetadataNameBloom))
                    {
                        byte[] bloom = matchEntry.Metadatas[UrlBinaryCodec.MetadataNameBloom] as byte[];

                        if (bloom != null)
                        {
                            Marshal.Copy(bloom, 0, childFilterPtr, (int)childFilterLength);
                        }
                    }

                    // Set noMatch to false
                    Marshal.WriteInt32(noMatchPtr, 0);
                }
                else
                {
                    // If we did not find a match for this then we have either a False Hit.
                    // or a top level domain (e.g. tempuri.org) that we have not seen in 
                    // our system yet.
                    // A False Hit means the SHA256 Appears in the bloom but the database 
                    // returns a No-Match.  
                    // In either case we cache a dummy object so that we do not go back to 
                    // the database for this SHA256 again.

                    matchEntry = MsasDataBlockProcessor.CreatePlaceholderObject(new ObjectIdentifier { Type = context.ObjectType, Sha256 = hash });

                    // Mark another No Match added to the cache
                    PerfCounters.LookupNoMatchObjectCached();

                    // Set noMatch to true
                    Marshal.WriteInt32(noMatchPtr, 1);
                }

                LogEventToNTLog(hResult, "HandleRequestChunkForResponse");

                context.MatchesOut.Add(matchEntry);
            }
            catch (Exception ex)
            {
                EventLogger.LogMrsCallbackResponseApiFailure(ex.Message);
                TraceUtility.TraceCatchException(Tracing.Source, ex);

                // TODO Set some form of hResult
                hResult = MRS_HRESULTS.S_FALSE;
            }

            return hResult;
        }

        internal static UnpackedObject CreatePlaceholderObject(ObjectIdentifier objectId)
        {
            UnpackedObject placeholder = new UnpackedObject
            {
                Type = objectId.Type,
                Sha256 = objectId.Sha256,
                Version = Int32.MinValue,
                ServerExpirationInMinutes = s_noMatchCacheTime,
                ClientExpirationInMinutes = s_noMatchCacheTime,
                Metadatas = new Dictionary<string, object>
                {
                    {UrlBinaryCodec.MetadataNameBloom, null}
                }
            };

            return placeholder;
        }

        private static void LogEventToNTLog(uint hresult, string methodName)
        {
            // There was some form of error while working with the MSAS API so we are just going to 
            // log it, and bail out
            if (hresult > MRS_HRESULTS.S_FALSE)
            {
                // hresult != (MSAS_HRESULTS.S_OK || MSAS_HRESULTS.S_FALSE)

                LogFailure(hresult, methodName);
            }
            else
            {
                LogSuccess(methodName);
            }
        }

        private static void LogSuccess(string methodName)
        {
            switch (methodName)
            {
                case "ProcessRequestForCache":
                    EventLogger.LogMrsProcessCacheApiSuccess();
                    break;
                case "ProcessRequestForDatabase":
                    EventLogger.LogMrsProcessDatabaseApiSuccess();
                    break;
                case "ProcessRequestForResponse":
                    EventLogger.LogMrsProcessResponseApiSuccess();
                    break;

                case "HandleRequestChunkForCache":
                    EventLogger.LogMrsCallbackCacheApiSuccess();
                    break;
                case "HandleRequestChunkForDatabase":
                    EventLogger.LogMrsCallbackDatabaseApiSuccess();
                    break;
                case "HandleRequestChunkForResponse":
                    EventLogger.LogMrsCallbackResponseApiSuccess();
                    break;
            }
        }

        private static void LogFailure(uint hresult, string methodName)
        {
            string failureMessage = string.Format(
                "Failure calling {0} in MSAS API.  HResult : {1:x}", methodName, hresult);

            switch (methodName)
            {
                case "ProcessRequestForCache":
                    EventLogger.LogMrsProcessCacheApiFailure(failureMessage);
                    break;
                case "ProcessRequestForDatabase":
                    EventLogger.LogMrsProcessDatabaseApiFailure(failureMessage);
                    break;
                case "ProcessRequestForResponse":
                    EventLogger.LogMrsProcessResponseApiFailure(failureMessage);
                    break;
                case "HandleRequestChunkForCache":
                    EventLogger.LogMrsCallbackCacheApiFailure(failureMessage);
                    break;
                case "HandleRequestChunkForDatabase":
                    EventLogger.LogMrsCallbackDatabaseApiFailure(failureMessage);
                    break;
                case "HandleRequestChunkForResponse":
                    EventLogger.LogMrsCallbackResponseApiFailure(failureMessage);
                    break;
            }
            TraceUtility.TraceError(Tracing.Source, failureMessage);
        }

        /// <summary>
        /// Determine if all of the inputs to the callback are safe for use in methods
        /// </summary>
        /// <param name="bufferPtr"></param>
        /// <param name="childFilterPtr"></param>
        /// <param name="checkChildFilter"></param>
        /// <param name="pvContext"></param>
        /// <returns></returns>
        private static uint CheckInputs(
            IntPtr bufferPtr, IntPtr childFilterPtr, bool checkChildFilter, IntPtr noMatchPtr, bool checkNoMatch, IntPtr pvContext)
        {
            if (IntPtr.Equals(bufferPtr, IntPtr.Zero))
            {
                // TODO Log an error and return a proper HRESULT
                return MRS_HRESULTS.S_FALSE;
            }

            if (checkChildFilter && IntPtr.Equals(childFilterPtr, IntPtr.Zero))
            {
                // TODO Log an error and return a proper HRESULT
                return MRS_HRESULTS.S_FALSE;
            }

            if (checkNoMatch && IntPtr.Equals(noMatchPtr, IntPtr.Zero))
            {
                // TODO Log an error and return a proper HRESULT
                return MRS_HRESULTS.S_FALSE;
            }

            if (IntPtr.Equals(pvContext, IntPtr.Zero))
            {
                // TODO Log an error and return a proper HRESULT
                return MRS_HRESULTS.S_FALSE;
            }

            return MRS_HRESULTS.S_OK;
        }

        internal static bool Equals(byte[] expected, byte[] actual)
        {
            if (expected == null && actual == null)
            {
                return true;
            }
            if ((expected == null && actual != null)
                || (expected != null && actual == null)
                || (expected.Length != actual.Length))
            {
                return false;
            }
            return Convert.ToBase64String(expected).Equals(Convert.ToBase64String(actual));
        }

    }
}